﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DimensionsHelper.Common.Contracts.Metadata;

namespace DimensionsHelper.Services.Metadata;

public enum MetadataObjectCollectionItemChangeType
{
    Add,
    Remove
}

public class MetadataObjectCollectionChangedEventArgs<T>(
    MetadataObjectCollectionItemChangeType changeType,
    T? oldItem,
    T? newItem)
    : EventArgs
    where T : class
{
    public MetadataObjectCollectionItemChangeType ChangeType { get; } = changeType;

    public T? OldItem { get; } = oldItem;

    public T? NewItem { get; } = newItem;
}


public abstract class MetadataObjectCollection<T> : IMetadataObjectCollection, IEnumerable<T>
    where T : class
{
    private readonly Node _deletedDummy = new(null!);
    protected readonly Node DummyHead = new(null!);
    protected int Version;

    public int Count { get; protected set; }

    public IEnumerable DeletedItems => new NodeEnumerable(_deletedDummy, this);

    public int DeletedItemsCount { get; protected set; }


    public object FirstObject => DummyHead.Next.Data;

    public object Get(int index)
    {
        return GetNode(index).Data;
    }

    public bool TryFind<TE>(Predicate<TE> predicate, out TE? result)
    {
        result = Find(predicate);
        return result != null;
    }

    public TE? Find<TE>(Predicate<TE> predicate)
    {
        var node = DummyHead.Next;

        while (node != DummyHead)
        {
            if (node.Data is TE element && predicate.Invoke(element))
            {
                return element;
            }

            node = node.Next;
        }

        return default;
    }


    public int CountIf<TE>(Predicate<TE> predicate)
    {
        var count = 0;
        var node = DummyHead.Next;

        while (node != DummyHead)
        {
            if (node.Data is TE element && predicate(element))
            {
                count++;
            }

            node = node.Next;
        }

        return count;
    }

    internal event EventHandler<MetadataObjectCollectionChangedEventArgs<T>>? ItemChanged;

    protected void CheckObjectCompatible(object item, Action<T>? callback)
    {
        if (item is T e)
        {
            callback?.Invoke(e);
        }
        else
        {
            throw new Exception($"添加的对象类型'{item.GetType().Name}'和集合类型'{typeof(T).Name}'不兼容。");
        }
    }

    protected virtual void OnBeforeChangingCollection(object item,
        MetadataObjectCollectionItemChangeType itemChangeType)
    {
        if (itemChangeType == MetadataObjectCollectionItemChangeType.Add)
        {
            CheckObjectCompatible(item, null);
        }
    }

    protected virtual void OnAfterCollectionChanged(object item,
        MetadataObjectCollectionItemChangeType itemChangeType)
    {
    }

    protected virtual void OnItemAdded(T newItem)
    {
        ItemChanged?.Invoke(this, new MetadataObjectCollectionChangedEventArgs<T>(
            MetadataObjectCollectionItemChangeType.Add,
            null, newItem));
    }

    protected virtual void OnItemRemoved(T oldItem)
    {
        ItemChanged?.Invoke(this, new MetadataObjectCollectionChangedEventArgs<T>(
            MetadataObjectCollectionItemChangeType.Remove,
            oldItem, null));
    }

    private static void AddNodeNext(Node next, Node dummyHead)
    {
        var last = dummyHead.Previous;
        last.Next = next;
        next.Previous = last;
        next.Next = dummyHead;
        dummyHead.Previous = next;
    }

    protected Node AddNode(T item)
    {
        OnBeforeChangingCollection(item, MetadataObjectCollectionItemChangeType.Add);

        var newNode = new Node(item);
        AddNodeNext(newNode, DummyHead);
        Count++;
        Version++;

        OnAfterCollectionChanged(item, MetadataObjectCollectionItemChangeType.Add);
        OnItemAdded(item);

        return newNode;
    }

    internal void Add(object item)
    {
        AddNode((T)item);
    }

    internal void AddDeletedItem(T obj)
    {
        var node = new Node(obj);
        AddNodeNext(node, _deletedDummy);
        DeletedItemsCount++;
    }

    private static Node GetNode(Node dummyHead, int index)
    {
        var i = index;
        var node = dummyHead;

        while (i >= 0 && node != dummyHead)
        {
            node = node.Next;
            i--;
        }

        if (i >= 0)
        {
            throw new IndexOutOfRangeException();
        }

        return node;
    }

    private Node GetNode(int index)
    {
        return GetNode(DummyHead, index);
    }

    private void InsertNode(Node insertNode, Node prevNode)
    {
        var nextNode = prevNode.Next;
        prevNode.Next = insertNode;
        insertNode.Next = nextNode;
        nextNode.Previous = insertNode;
        insertNode.Previous = prevNode;
        Count++;
        Version++;
    }


    internal void Insert(object item, int index)
    {
        if (index < 0)
        {
            throw new IndexOutOfRangeException();
        }

        OnBeforeChangingCollection(item, MetadataObjectCollectionItemChangeType.Add);
        var newNode = new Node((T)item);
        if (index == 0 && Count == 0)
        {
            AddNodeNext(newNode, DummyHead);
        }
        else
        {
            var node = GetNode(index);
            InsertNode(newNode, node.Previous);
        }

        OnAfterCollectionChanged(item, MetadataObjectCollectionItemChangeType.Add);
        OnItemAdded((T)item);
    }

    protected void DeleteNode(Node node)
    {
        if (node == DummyHead)
        {
            return;
        }

        var prev = node.Previous;
        var next = node.Next;

        prev.Next = next;
        next.Previous = prev;

        Count--;
        Version++;
    }

    internal bool RemoveAt(int index)
    {
        if (index < 0 || index >= Count)
        {
            return false;
        }

        var node = GetNode(index);
        OnBeforeChangingCollection(node.Data, MetadataObjectCollectionItemChangeType.Remove);
        DeleteNode(node);
        OnAfterCollectionChanged(node.Data, MetadataObjectCollectionItemChangeType.Remove);
        OnItemRemoved(node.Data);
        return true;
    }


    public virtual void Clear()
    {
        DummyHead.Previous = DummyHead;
        DummyHead.Next = DummyHead;
        Count = 0;
        Version++;
    }

    public class Node
    {
        public Node(T data)
        {
            Data = data;
            Previous = this;
            Next = this;
        }


        public Node Previous { get; internal set; }

        public Node Next { get; internal set; }

        public T Data { get; }
    }

    private readonly struct NodeEnumerable(Node node, MetadataObjectCollection<T> collection) : IEnumerable<T>
    {
        private readonly IEnumerator<T> _enumerator = new Enumerator(node, collection);

        public IEnumerator<T> GetEnumerator()
        {
            return _enumerator;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _enumerator;
        }
    }


    #region IEnumerable

    private struct Enumerator : IEnumerator<T>
    {
        private readonly MetadataObjectCollection<T> _collection;
        private readonly Node _dummy;
        private readonly int _version;
        private Node _current;

        internal Enumerator(Node dummyHead, MetadataObjectCollection<T> collection)
        {
            _dummy = dummyHead;
            _version = collection.Version;
            _collection = collection;
            _current = _dummy;
        }

        public readonly T Current => _current.Data;

        readonly object IEnumerator.Current => _current.Data;

        public readonly void Dispose()
        {
        }

        private readonly void CheckVersion()
        {
            if (_version != _collection.Version)
            {
                throw new InvalidOperationException();
            }
        }

        public bool MoveNext()
        {
            CheckVersion();

            if (_current.Next == _dummy)
            {
                return false;
            }

            _current = _current.Next;
            return true;
        }

        public void Reset()
        {
            CheckVersion();
            _current = _dummy;
        }
    }


    protected virtual IEnumerator<T> GetEnumeratorOverride()
    {
        return new Enumerator(DummyHead, this);
    }

    public IEnumerator GetEnumerator()
    {
        return GetEnumeratorOverride();
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return GetEnumeratorOverride();
    }

    #endregion
}


public abstract class MetadataStringKeyCollection<T> : MetadataObjectCollection<T>, IMetadataStringKeyCollection<T>
    where T : class
{
    protected readonly Dictionary<string, Node> ItemMap;

    protected MetadataStringKeyCollection()
    {
        ItemMap = new Dictionary<string, Node>(StringComparer.OrdinalIgnoreCase);
        DeletedItemsCount = 0;
    }

    public abstract T CreateObject(string key);

    public T this[int index] => (T)Get(index);

    public bool Exists(string name)
    {
        return ItemMap.ContainsKey(name);
    }

    public virtual T GetByKey(string key)
    {
        if (ItemMap.TryGetValue(key, out var node))
        {
            return node.Data;
        }

        var newObject = CreateObject(key);
        Add(newObject);
        return newObject;
    }

    public bool TryGetByKey(string key, out T value)
    {
        value = null!;

        if (!ItemMap.TryGetValue(key, out var node))
        {
            return false;
        }

        value = node.Data;
        return true;
    }

    protected abstract string KeySelector(T item);

    private void CheckObjectKey(T e)
    {
        var key = KeySelector(e);

        if (ItemMap.ContainsKey(key))
        {
            throw new Exception($"关键字'{key}'已存在，当前类型是{typeof(T).Name}");
        }
    }

    protected override void OnBeforeChangingCollection(object item,
        MetadataObjectCollectionItemChangeType itemChangeType)
    {
        if (itemChangeType == MetadataObjectCollectionItemChangeType.Add)
        {
            CheckObjectCompatible(item, CheckObjectKey);
        }
    }

    public override void Clear()
    {
        base.Clear();
        ItemMap.Clear();
    }

    internal void Add(T item)
    {
        var node = AddNode(item);
        ItemMap.Add(KeySelector(item), node);
        OnItemAdded(item);
    }

    private bool Remove(string name)
    {
        if (!ItemMap.TryGetValue(name, out var node))
        {
            return false;
        }

        OnBeforeChangingCollection(node.Data, MetadataObjectCollectionItemChangeType.Remove);
        ItemMap.Remove(name);
        DeleteNode(node);
        OnAfterCollectionChanged(node.Data, MetadataObjectCollectionItemChangeType.Remove);
        OnItemRemoved(node.Data);
        return true;
    }

    internal bool Remove(T item)
    {
        return Remove(KeySelector(item));
    }
}


public abstract class MetadataContextCollection<T> : MetadataStringKeyCollection<T>, IMetadataContextCollection<T>
    where T : class, IMetadataContextObject
{
    protected internal MetadataContextCollection(IMetadataDocument document, ObjectType objectType,
        IMetadataObject parent)
    {
        Document = document;
        ObjectType = objectType;
        Parent = parent;
    }

    public bool Exists(string name, string context)
    {
        return ItemMap.ContainsKey(MakeKey(name, context));
    }

    public T Get(string name, string context)
    {
        var key = MakeKey(name, context);
        return GetByKey(key);
    }


    public abstract T CreateObject(string name, string context);


    public IEnumerable<T> Filter(string context)
    {
        var node = DummyHead.Next;

        while (node != DummyHead)
        {
            if (node.Data.Context.Equals(context, StringComparison.OrdinalIgnoreCase))
            {
                yield return node.Data;
            }

            node = node.Next;
        }
    }

    public T1? FindParent<T1>() where T1 : IMetadataObject
    {
        return MetadataObject.FindParent<T1>(this);
    }

    public int CountIf(string context)
    {
        return ItemMap.Keys.Count(key => key.StartsWith(MakeContextPrefix(context), StringComparison.OrdinalIgnoreCase));
    }

    private static string MakeContextPrefix(string context)
    {
        return $"C:{context}-";
    }

    protected static string MakeKey(string name, string context)
    {
        return $"{MakeContextPrefix(context)}N:{name}";
    }

    protected override string KeySelector(T item)
    {
        return MakeKey(item.Name, item.Context);
    }

    protected static ElementInfo GetElementInfoFromKey(string key)
    {
        var name = "";
        var context = "";

        if (key.Length <= 6)
        {
            return new ElementInfo { Context = context, Name = name };
        }

        var sepPos = key.IndexOf('-');
        if (sepPos < 0)
        {
            return new ElementInfo { Context = context, Name = name };
        }

        if (key.StartsWith("C", StringComparison.OrdinalIgnoreCase))
        {
            context = key[2..sepPos];
        }

        if (key.Length - sepPos > 3)
        {
            name = key.Substring(sepPos + 3, key.Length - sepPos - 3);
        }

        return new ElementInfo { Context = context, Name = name };
    }

    protected struct ElementInfo
    {
        public string Name;
        public string Context;

        public readonly bool IsValid()
        {
            return !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Context);
        }
    }

    #region IMetadataObject

    public IMetadataDocument Document { get; }

    public ObjectType ObjectType { get; }

    public IMetadataObject Parent { get; internal set; }

    public IVersionObjectCollection? Versions { get; internal set; }

    public bool Versioned { get; set; }

    #endregion
}


public abstract class MetadataLabeledCollection<T> : MetadataStringKeyCollection<T>, IMetadataLabeledObject
    where T : class
{
    protected MetadataLabeledCollection(IMetadataDocument document, ObjectType type,
        IMetadataObject? parent = null)
    {
        Document = document;
        ObjectType = type;
        Id = Guid.NewGuid().ToString();
        Parent = parent;
        Properties = new Properties(document, this);
        Versions = null;
        Label = new MetadataLabels(document);
        Name = string.Empty;
        Style = null;
        LabelStyle = null;
        Templates = new Templates(document, this);
        Notes = new Notes(document, this);
        Versioned = true;
    }

    #region IMetadataLabeledObject

    public string Id { get; internal set; }

    public string Name { get; protected set; }

    public string FullName => MetadataNamedObject.GetFullName(this);

    public IMetadataDocument Document { get; }

    public ObjectType ObjectType { get; protected set; }

    public IProperties Properties { get; }

    public IMetadataLabels Label { get; }

    public IMetadataObject? Parent { get; protected set; }

    public IVersionObjectCollection? Versions { get; internal set; }

    public IStyles? Style { get; set; }

    public IStyles? LabelStyle { get; set; }

    public ITemplates Templates { get; }

    public INotes Notes { get; }

    public bool Versioned { get; set; }

    public T1? FindParent<T1>() where T1 : IMetadataObject
    {
        return MetadataObject.FindParent<T1>(this);
    }

    public void SetProperty(string name, string value)
    {
        Properties.Get(name, Document.Context).SetValue(value);
    }

    public void SetProperty(string name, int value)
    {
        Properties.Get(name, Document.Context).SetValue(value);
    }

    public void SetProperty(string name, double value)
    {
        Properties.Get(name, Document.Context).SetValue(value);
    }

    public void SetProperty(string name, bool value)
    {
        Properties.Get(name, Document.Context).SetValue(value);
    }

    public void SetProperty(string name, IProperty property)
    {
        Properties.Get(name, Document.Context).SetValue(property);
    }

    public void SetProperty(string name, IProperties properties)
    {
        Properties.Get(name, Document.Context).SetValue(properties);
    }

    #endregion
}